import java.awt.image.BufferedImage;

import java.awt.image.WritableRaster;

import edu.rit.pj.IntegerForLoop;
import edu.rit.pj.IntegerSchedule;
import edu.rit.pj.ParallelForLoop;
import edu.rit.pj.ParallelRegion;
import edu.rit.pj.ParallelTeam;


public class Morph {

    // This describes how often the GUI will redraw the morphed picture
    // Set this to a higher value, like 16, for larger jobs.
    public static int UPDATE_STEPS = 8;
    public static int inner_steps;

    //take a larger stepping factor for faster morphing 
    public static int FAST_STEPPING = 4;
    
    // The total number of iterations may be as high as 256 becuase
    // the difference in color-intensity in some point may be 256 for some color
    public static int MORPH_ITERATIONS = 256/FAST_STEPPING + FAST_STEPPING;

    // After cropping, height/width should be equal for both images.
    public static int height;
    public static int width;
    
    public static BufferedImage morph;
    public static WritableRaster morphRaster;
 
    static int [][] array1;
    static int [][] array2;
     
    static MorphImg gui;
    
    public static int numThreads =1 ;
    public static boolean displayOff= false;
    
    private Morph(){}
    
    public static void setNumThreads(int threads)
    {
    	numThreads = threads; 
    }
    
    public static void setUpdateSteps(int steps)
    {
    	UPDATE_STEPS = steps;
    }
    public static void setFastStepping(int steps)
    {
    	FAST_STEPPING = steps;
	MORPH_ITERATIONS = 256/FAST_STEPPING + FAST_STEPPING;
    }
    
    public static void setDisplayOff(boolean c)
    {
    	displayOff = c;
    }
 
    public static void MorphInit(BufferedImage image1, BufferedImage image2, MorphImg guiapp) {
    	
    	gui = guiapp;
    		
	// Double check that we clipped the images at read in time
	// if not the same size
        assert(image1.getWidth() == image2.getWidth());
        assert(image1.getHeight() == image2.getHeight());
        height = image1.getHeight();
        width = image1.getWidth();


        int[] image1pixels = image1.getRaster().getPixels(0, 0, width, height, (int[])null);
        int[] image2pixels = image2.getRaster().getPixels(0, 0, width, height, (int[])null);

	// The gui displays the morphed image, which initially is the
	// "From" image
        morph = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
	// Return a  WritableRaster, an internal reprentation of the
	// image (class Raster), that we can modify
        morphRaster = morph.getRaster();
	//Copy the "from" image and update the raster
        morphRaster.setPixels(0, 0, width, height, image1pixels);
        gui.updateMorphImage(morph, false);

	// Generate the 3D representations of the images
        array1 = convertTo2DArray(image1pixels, width, height);
        array2 = convertTo2DArray(image2pixels, width, height);
    }

     
    //redraws only if display mode is on
    static void redrawPicture(int  [][]array1)
    {
  
        if(!displayOff){
	    //gui.freezeTimer();
	    int[] arr1D = convertTo1DArray(array1, width, height);
	    morphRaster.setPixels(0, 0, width, height, arr1D);
	    gui.updateMorphImage(morph, false);
	    // update the reported timing
	    gui.updateTimer(); 
	    //gui.unFreezeTimer();
        
        }
    }

// When you parallelize the code, add your own Morph routine here
// The Morphing algorithm should be the same with the serial code, but you will add
// parallel control flow and synchronization.
    public static BufferedImage parallelDoMorph() {       
    	final int last_iteration = MORPH_ITERATIONS % UPDATE_STEPS;
        
    	gui.startTimer();
    	try{
        new ParallelTeam(numThreads).execute(new ParallelRegion() {
		    public void run(){
		    	int tid = getThreadIndex();
		    	for(int h = 0; h < MORPH_ITERATIONS; h += UPDATE_STEPS ) {
		    	    // MORPH
		    	    final int steps = (h + UPDATE_STEPS) > MORPH_ITERATIONS ? last_iteration : UPDATE_STEPS; 
		    		
		    	    try {
						execute(0,array1.length - 1, new IntegerForLoop(){
							public IntegerSchedule schedule(){
								return IntegerSchedule.dynamic();
							}
							
							public void run(int first, int last){
							    for(int i=0 ; i < steps; i++){

								morphTick(array1, array2, first, last);
								
							    }
							}
						});
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
		    	    if (tid == 0) {
		    	    	gui.updateTimer();
		    	    	// Redraw the pictures, for "animation".
		    	    	redrawPicture(array1);
		    	    }
		    	    barrier();
		    	}
		    }

        });
    	}catch(Exception e){
		e.printStackTrace();
	}
    	
    	// We are done timing now
    	gui.stopTimer();

    	// Add a done label, if we are using the display
    	gui.updateDoneLabel();
            
    	// Saves the last image into a raster
    	int[] arr1D = convertTo1DArray(array1, width, height);
    	morphRaster.setPixels(0, 0, width, height, arr1D);
    	return morph;
    }


// The Morphing code, including display
// The actual morphing is done in morphTick
    public static BufferedImage serialDoMorph() {
        
	int last_iteration = MORPH_ITERATIONS % UPDATE_STEPS;
            
	gui.startTimer();
            
	for(int h = 0; h < MORPH_ITERATIONS; h += UPDATE_STEPS ) {
	    // MORPH
	    int steps = (h + UPDATE_STEPS) > MORPH_ITERATIONS ? last_iteration : UPDATE_STEPS; 
		    
	    for(int i=0 ; i < steps; i++){

		morphTick(array1, array2,0,array1.length-1);
	    }
	    gui.updateTimer();
	    
	   // Redraw the pictures, for "animation".
	   redrawPicture(array1);
	}
            
	
	// We are done timing now
	gui.stopTimer();

	// Add a done label, if we are using the display
	gui.updateDoneLabel();
        
	// Saves the last image into a raster
	int[] arr1D = convertTo1DArray(array1, width, height);
	morphRaster.setPixels(0, 0, width, height, arr1D);
        	
	return morph;
    }

 
    /**
     * @param image1 is modified, with each pixel modified to look one 
     *         quantum level (or FAST_STEPPING) more like
     *         the corresponding pixel in image2.
     */
        
    private static void morphTick(int[][][] image1, int[][][] image2) {
    	//row
    	for(int i = 0; i < image1.length; i++){
	    // column
	    for(int j = 0; j < image1[0].length; j++){
  
		// colors: red, green and blue
		// calculate the difference between two images for each color
		int diffr = (image2[i][j][0] - image1[i][j][0]);
		int diffg = (image2[i][j][1] - image1[i][j][1]);
		int diffb = (image2[i][j][2] - image1[i][j][2]);
    	 
                //signum() returns -1, 0 or 1.  
		int redstep = (int)Math.signum(diffr);
		int greenstep = (int) Math.signum(diffg);
		int bluestep = (int)Math.signum(diffb);
		
		int adiffr = Math.abs(diffr);
		int adiffg = Math.abs(diffg);
		int adiffb = Math.abs(diffb);

		//take a big or small step
		if (adiffr >= FAST_STEPPING)
		    redstep *= (int) Math.min(adiffr,FAST_STEPPING);
		if (adiffg >= FAST_STEPPING)
		    greenstep *= (int) Math.min(adiffg,FAST_STEPPING);
		if (adiffb >= FAST_STEPPING)
		    bluestep *= (int) Math.min(adiffb,FAST_STEPPING);
    					
		//update the source image
		image1[i][j][0] += redstep;
		image1[i][j][1] += greenstep;
		image1[i][j][2] += bluestep;

	    }
        }
    }
    
    /**
     * @param image1 is modified, with each pixel modified to look one 
     *         quantum level (or FAST_STEPPING) more like
     *         the corresponding pixel in image2.
     */
        
    private static void morphTick(int[][] image1, int[][] image2, int first, int last) {
    	//row
    	for(int i = first; i <= last; i++){
	    // column
	    for(int j = 0; j < (image1[0].length-32)/3; j++){
  
		// colors: red, green and blue
		// calculate the difference between two images for each color 	
		int diffr = (image2[i][j*3] ) - (image1[i][j*3] );
		int diffg = (image2[i][j*3+1] ) - (image1[i][j*3+1] );
		int diffb = (image2[i][j*3+2] ) - (image1[i][j*3+2]);
    	 
                //signum() returns -1, 0 or 1.  
		int redstep = (int)Math.signum(diffr);
		int greenstep = (int) Math.signum(diffg);
		int bluestep = (int)Math.signum(diffb);
		
		int adiffr = Math.abs(diffr);
		int adiffg = Math.abs(diffg);
		int adiffb = Math.abs(diffb);

		//take a big or small step
		if (adiffr >= FAST_STEPPING)
		    redstep *= (int) Math.min(adiffr,FAST_STEPPING);
		if (adiffg >= FAST_STEPPING)
		    greenstep *= (int) Math.min(adiffg,FAST_STEPPING);
		if (adiffb >= FAST_STEPPING)
		    bluestep *= (int) Math.min(adiffb,FAST_STEPPING);
    					
		//update the source image
		image1[i][j*3] += redstep;
		image1[i][j*3+1] += greenstep;
		image1[i][j*3+2] += bluestep;

	    }
        }
    }
    
    // Unpack a 1D image array into a 2D array
     public static int[][] convertTo2DArray( int[] oneDPix1, int width, int height){
    	 
        int[][] data = new int[height][width*3+32]; 

        // Convert 1D array to 3D array
        for(int row = 0; row < height; row++){
            for(int col = 0; col < width; col++){
                int element = (row * width + col)*3;
                // Red
                data[row][col*3] = oneDPix1[element+0];
                // Green
                data[row][col*3+1] = oneDPix1[element+1];
                // Blue
                data[row][col*3+2] = oneDPix1[element+2];
                               
            }
        }
        return data;
    }

    
    public static int[] convertTo1DArray( int[][] data, int width, int height){

	// Pack a 3D image array into a 1D array because raster requires 1D int array
        int[] oneDPix = new int[ width * height * 3];

        int cnt = 0;
      
        for (int row = 0; row < height; row++){
            for (int col = 0; col < width; col++){
            	//red
                oneDPix[cnt++] =  data[row][col*3];
                //green
                oneDPix[cnt++] =  data[row][col*3+1];
                //blue
                oneDPix[cnt++] =  data[row][col*3+2];
            }
        }

        return oneDPix;
    }
    
    /*public static int[] convertTo1DArray( int[][] data, int width, int height){

    	// Pack a 2D image array into a 1D array because raster requires 1D int array
            int[] oneDPix = new int[ width * height ];

            int cnt = 0;
          
            for (int row = 0; row < height; row++){
                for (int col = 0; col < width; col++){
                    oneDPix[cnt++] =  data[row][col];
                }
            }

            return oneDPix;
        }*/
}

